iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0

今天還是再深入一點介紹dataclass(註1)好了。


擴充性

  • 這是昨天「傳統版」的code:
    class Tree():
        '''為了聚焦本class僅實作getters,略去setters, deleters。'''
        def __init__(self, breed: str, age: int, height: int):
            self.__breed = breed
            self.__age = age
            self.__height = height
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:
            '''The age property(getter).'''
            return self.__age
    
        @property
        def height(self) -> int:
            '''The height property(getter).'''
            return self.__height
    
        # 真要設計完整的class,還得實作以下的boilerplate code。
        def __repr__(self) -> str:
            return f'Tree({self.breed=}, {self.age=}, {self.height=})'
    
        def __hash__(self):
            return hash((self.__class__, self.breed, self.age, self.height))
    
        def __eq__(self, other):
            if other.__class__ is self.__class__:
                return self.breed == other.breed and self.age == other.age and self.height == other.height
            else:
                return NotImplemented
    
  • 試想一下:如果在類別設計好之後,才發覺當初思慮不周,這時需求也有所改變,必須增加id(int)和location(str)兩個屬性。於是,類別只好大動干戈,所有boilerplates都得修改,工程有點大,有人認為這種改動,幾乎等於「重構」(refactor):
    class Tree():
        '''為了聚焦本class僅實作getters,略去setters, deleters。'''
        def __init__(self, id: int, breed: str, age: int, height: int, ):
            self.__breed = breed
            self.__age = age
            self.__height = height
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:
            '''The age property(getter).'''
            return self.__age
    
        @property
        def height(self) -> int:
            '''The height property(getter).'''
            return self.__height
    
        # 真要設計完整的class,還得實作以下的boilerplate code。
        def __repr__(self) -> str:
            return f'Tree({self.breed=}, {self.age=}, {self.height=})'
    
        def __hash__(self):
            return hash((self.__class__, self.breed, self.age, self.height))
    
        def __eq__(self, other):
            if other.__class__ is self.__class__:
                return self.breed == other.breed and self.age == other.age and self.height == other.height
            else:
                return NotImplemented
    
    測試程式:
    tree1 = Tree(1, 'cedar', 50, 200, 'X235.6')
    print(f'\n{tree1}')
    
    tree2 = Tree(2, 'beech', 260, 307, 'J103.0')
    print(f'\n{tree2}')
    
    輸出:
  • 程式修改之後輸出正確是正確,不過這樣動不動就得修改boilerplates,不是很繁瑣嗎?
  • 如果當初用的是dataclass,這時要做的只是加兩個屬性,game over。當然getters / setters等還是要自己寫。
    from dataclasses import dataclass
    
    @dataclass
    class Tree():
        __id: int         # 新增
        __breed: str 
        __age: int   
        __height: int
        __location: str   # 新增
    
        @property
        def id(self) -> int:
            '''The id property(getter).'''
            return self.__id
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:
            '''The age property(getter).'''
            return self.__age
    
        @property
        def height(self) -> int:
            '''The height property(getter).'''
            return self.__height
    
        @property
        def location(self) -> str:
            '''The location property(getter).'''
            return self.__location    
    
        # 這時不用寫那幾個boilerplates了。dataclass會自動幫我們產生。
    
    測試程式同上。
    輸出:
  • 結論:使用dataclass,可以改善類別boilerplate code的擴充性。因為使用了dataclass,就幾乎不必自己動手寫boilerplate code,省去很多複製貼上再修修補補的重複且無聊苦工。

Dataclass的參數

  • dataclass decorator本身有一些參數以供調校,增加彈性。Python 3.10有下列參數(註2):

    • init:為True(預設)時產生建構子__init__()。筆者按:這個參數是否有點多餘?當然要產生呀?否則要dataclass何用?
    • repr:為True(預設)時產生__repr()方法。如已有自行設計的__repr__(),以自訂者為準,dataclass甘拜下風。
    • eq:為True(預設)時產生__eq__()方法。如已有自行設計的__eq__(),dataclass從之。
    • order:為True(預設False)時產生以下方法:__lt__(), __le__(), __gt__(),及__ge__()。如order為True而eq為False會觸發ValueError。另外,如有自訂的__lt__(), __le__(), __gt__(), __ge__(),則賞您一個TypeError。
    • unsafe_hash:為False(預設)時依eqfrozen如何設定來產生__hash__()方法。詳細說明請直接看官網。
    • frozen:此冰凍參數為True(預設False)時,物件一旦建造,屬性就無法修改,變成immutable dataclass,性質有點像tuple。
    • match_args:為True(預設)時產生__match_args__tuple。
    • kw_only:為True(預設False)時所有屬性均標記為keyword-only。詳見官網。
    • slots:為True(預設False)時產生__slots__屬性。如有自訂__slots__會拋出TypeError。
  • 驗證。注意decorator有加參數,先看frozen=True的效果:

    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Tree():
        __id: int         # 新增
        __breed: str 
        __age: int   
        __height: int
        __location: str   # 新增
    
        @property
        def id(self) -> int:
            '''The id property(getter).'''
            return self.__id
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:
            '''The age property(getter).'''
            return self.__age
    
        @age.setter     # 本版順手補上setter。
        def age(self, age: int):
            '''The age property(setter).'''
            if isinstance(age, bool) or not isinstance(age, int):
                raise TypeError('樹齡必須是整數。')
            # 以下的條件判斷只是「示意」,實際上該和breed一併考慮才對。
            if age > 15_000 or age < 0:
                raise Exception(f'樹齡數字{age}不合理。')
            self.__age = age
    
        @property
        def height(self) -> int:
            '''The height property(getter).'''
            return self.__height
    
        @property
        def location(self) -> str:
            '''The location property(getter).'''
            return self.__location    
    

    主程式:

    try:
        tree = Tree(1, 'cedar', 50, 200, 'X235.6')
        print(f'\nbefore assignment: {tree.age = :<10,}')
        tree.age = 100
    except Exception as e:
        print(str(e))    
    finally:
        print(f'\nafter assignment : {tree.age = :<10,}')
    

    輸出(屬性遭凍結無法修改):
    https://ithelp.ithome.com.tw/upload/images/20220929/20148485Kn2z6kX2dg.png

  • 再將frozen參數改回False:

    from dataclasses import dataclass
    
    @dataclass(order=True, frozen=False)
    class Tree():
        __id: int         # 新增
        __breed: str 
        __age: int   
        __height: int
        __location: str   # 新增
    
        @property
        def id(self) -> int:
            '''The id property(getter).'''
            return self.__id
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:
            '''The age property(getter).'''
            return self.__age
    
        @age.setter     # 本版順手補上setter。
        def age(self, age: int):
            '''The age property(setter).'''
            if isinstance(age, bool) or not isinstance(age, int):
                raise TypeError('樹齡必須是整數。')
            # 以下的條件判斷只是「示意」,實際上該和breed一併考慮才對。
            if age > 15_000 or age < 0:
                raise Exception(f'樹齡數字{age}不合理。')
            self.__age = age
    
        @property
        def height(self) -> int:
            '''The height property(getter).'''
            return self.__height
    
        @property
        def location(self) -> str:
            '''The location property(getter).'''
            return self.__location    
    

    主程式不變,這回修改年齡成功,表示屬性不是immutable:
    https://ithelp.ithome.com.tw/upload/images/20220929/20148485eSqaQIfQZq.png

field()方法

  • Dataclass decorator另外提供field()方法。這裡dataclass的用詞是field欄位,指的其實就是屬性attribute。為了和系列其他文章用詞一致,筆者還是用屬性而不用欄位

  • field()方法功用是針對每一個屬性作個別設定,例如設定某屬性的預設值。請注意,預設值設定依然得遵守Python函數參數傳遞的老規距:先位置型參數後關鍵字參數:

    from dataclasses import dataclass, field
    
    @dataclass
    class Tree():
        __id: int        
        __age: int   
        __height: int
        __location: str = field(default='X203.9')  # similar to __location: str = 'X203.9'
        __breed: str = field(default='cedar')  # similar to __breed: str = 'cedar'
    
        @property
        def id(self) -> int:
            '''The id property(getter).'''
            return self.__id
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:
            '''The age property(getter).'''
            return self.__age
    
        @property
        def height(self) -> int:
            '''The height property(getter).'''
            return self.__height
    
        @property
        def location(self) -> str:
            '''The location property(getter).'''
            return self.__location    
    

    主程式:

    tree = Tree(1, 260, 307)   # 只給3個位置型參數。
    print(f'\n{tree}\n')     
    

    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220929/20148485cjepwIQetm.png

  • Python 3.10版的dataclass,field()方法的所有參數如下,有些參數的意義筆者不明所以,只得原文照抄:

    • default:提供本屬性的預設值。
    • default_factory:如有設定本參數,it must be a zero-argument callable that will be called when a default value is needed for this field.(一時未能消化其意,只好直接貼上原文)。本參數不能和default參數共存。
    • init:為True(預設)時建構子__init__()納入本屬性。
    • repr:為True(預設)時__repr__()方法包含本屬性。
    • hash:本參數可為bool或None。True時__hash__()方法將包含本屬性。None(預設)時視compare參數而定。
    • compare:為True(預設)時__eq__(), __gt__()等方法將包含本屬性。
    • metadata:可以是一個mapping或者為None。本參數包裹(wrapped)於MappingProxyType()方法內,使得該屬性成為唯讀,或曝露於Field物件。這句話的真義筆者也不了解,原文是:This can be a mapping or None. None is treated as an empty dict. This value is wrapped in MappingProxyType() to make it read-only, and exposed on the Field object.
    • kw_only:為True時本屬性會標記為keyword-only。
  • 小試field()方法:

    from dataclasses import dataclass, field
    
    @dataclass
    class Tree():
        __id: int        
        __age: int = field(init=False, repr=False)  
        __height: int = field(repr=False)
        __location: str = field(default='X203.9')  # similar to __location: str = 'X203.9'
        __breed: str = field(default='cedar')  # similar to __breed: str = 'cedar'
    
        @property
        def id(self) -> int:
            '''The id property(getter).'''
            return self.__id
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:
            '''The age property(getter).'''
            return self.__age
    
        @property
        def height(self) -> int:
            '''The height property(getter).'''
            return self.__height
    
        @property
        def location(self) -> str:
            '''The location property(getter).'''
            return self.__location    
    

    測試程式:

    try: 
        tree = Tree(1, 307)
        print(f'\n{tree}\n') 
        print(f'{tree.location=}')
        print(f'{tree.age=}')
    except Exception as e:
        print(str(e))        
    

    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220929/20148485Z5NmDqofWR.png

  • 說明:

    • __age屬性故意設為init=False, repl=False,使得建構子不納入本屬性。筆者測試的結果是init=Falserepl也須設為False,否則在企圖執行__repl__()方法時會找不到'_Tree__age'
    • __height屬性設為repr=False,結果是該屬性不會納入__repl__(),亦即print(f'\n{tree}\n')這行時,不會印出樹高。
    • 至於__location__breed兩屬性給了預設值,上面已有交代,不再贅述。

註1: 本篇時而僅說dataclass,時而用dataclass decorator,兩者意義並無差異。
註2:這些參數和field()的參數均參考自Python官網3.10.7版文件


上一篇
自動產生Boilerplate Code的好幫手:Dataclass
下一篇
Class Attributes
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言